/*****************************************************************************
 * Copyright (c) 2010 Atos Origin.
 *
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *  Tristan FAURE tristan.faure@atosorigin.com - Initial API and implementation
 *
 *****************************************************************************/
package org.eclipse.papyrus.infra.core.modelsetquery.impl;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EContentAdapter;
import org.eclipse.papyrus.infra.core.modelsetquery.IFillableModelSetQueryAdapter;

/**
 * This cache creates a map associating EClasses to all the corresponding
 * This implementation takes less space but it is less performant for get and put methods
 * instances
 *
 * @author Tristan Faure
 */
public class ModelSetQueryAdapterSizeMatters extends EContentAdapter implements IFillableModelSetQueryAdapter {

	/**
	 * The cache of elements
	 */
	private Map<EClassifier, Collection<EObject>> cache = Collections.synchronizedMap(new IdentityHashMap<EClassifier, Collection<EObject>>());
	private Map<EClassifier, Collection<EClassifier>> subTypes = Collections.synchronizedMap(new IdentityHashMap<EClassifier, Collection<EClassifier>>());


	public ModelSetQueryAdapterSizeMatters() {
		super();
	}

	@Override
	protected void addAdapter(Notifier notifier) {
		super.addAdapter(notifier);
		if (notifier instanceof EObject) {
			EObject eobject = (EObject) notifier;
			addObjectInCache(eobject);
		}
	}

	@Override
	protected void removeAdapter(Notifier notifier) {
		super.removeAdapter(notifier);
		if (notifier instanceof EObject) {
			EObject eobject = (EObject) notifier;
			removeObjectFromCache(eobject);
		}
	}

	private void addObjectInCache(EObject newObj) {
		putObjectInCache(newObj);
	}

	private void putObjectInCache(EObject newObj) {
		putObjectInMap(newObj.eClass(), newObj);
		addSubTypes(newObj.eClass());
	}

	private void addSubTypes(EClass eClassifier) {
		for (EClass superType : eClassifier.getESuperTypes()) {
			addSubType(superType, eClassifier);
			addSubTypes(superType);
		}
	}

	protected void addSubType(EClass superType, EClassifier eClassifier) {
		Collection<EClassifier> result = subTypes.get(superType);
		if (result == null) {
			result = new HashSet<EClassifier>();
			subTypes.put(superType, result);
		}
		result.add(eClassifier);
	}

	private void putObjectInMap(EClassifier eClassifier, EObject obj) {
		Collection<EObject> result = cache.get(eClassifier);
		if (result == null) {
			result = new HashSet<EObject>();
			cache.put(eClassifier, result);
		}
		result.add(obj);
	}

	private void removeObjectFromCache(EObject newObj) {
		EClass eClass = newObj.eClass();
		removeObjectFromCache(eClass, newObj);
	}

	private void removeObjectFromCache(EClassifier eClassifier, EObject newObj) {
		Collection<EObject> listOfClassifiers = cache.get(eClassifier);
		if (listOfClassifiers != null) {
			listOfClassifiers.remove(newObj);
			if (listOfClassifiers.isEmpty()) {
				cache.remove(eClassifier);
			}
		}
	}

	@Override
	public Collection<EObject> getReachableObjectsOfType(EObject object, EClassifier type) {
		Set<EObject> buffer = new HashSet<EObject>();
		Set<EClassifier> alreadyComputed = new HashSet<EClassifier>();
		Stack<EClassifier> types = new Stack<EClassifier>();
		types.push(type);
		while (!types.isEmpty()) {
			EClassifier top = types.pop();
			alreadyComputed.add(top);
			// add instances to the buffer
			Collection<EObject> c = cache.get(top);
			if (c != null) {
				buffer.addAll(c);
			}
			// compute sub types
			Collection<EClassifier> c2 = subTypes.get(top);
			if (c2 != null) {
				for (EClassifier eClassifier : c2) {
					if (eClassifier != null && !alreadyComputed.contains(eClassifier)) {
						types.add(eClassifier);
					}
				}
			}

		}
		return buffer;
	}

	public void dispose() {
		cache.clear();
		subTypes.clear();
		cache = null;
		subTypes = null;
	}

	/**
	 * This method provides a way for user to force first entries in the cache.
	 * The list of element must be a HashSet to optimize the performances
	 *
	 * @param type
	 * @param list
	 */
	@Override
	public void addEntriesInCache(EClassifier type, HashSet<EObject> list) {
		for (EObject e : list) {
			addObjectInCache(e);
		}
	}

	@Override
	public boolean isAlreadyComputed(EClassifier type) {
		return cache.containsKey(type);
	}



}
